/**
 * 
 */
package edu.umd.clip.lm.questions;

import edu.umd.clip.lm.model.*;
import edu.umd.clip.lm.model.data.Context;
import edu.umd.clip.lm.model.training.*;
import edu.umd.clip.lm.util.*;

import org.w3c.dom.*;

import java.io.Serializable;
import java.util.*;

/**
 * @author Denis Filimonov <den@cs.umd.edu>
 *
 */
public abstract class Question extends XMLizable implements Serializable {
	private static final long serialVersionUID = 1L;
	public final static int IDENTITY_QUESTION = 1;
	public final static int BINARY_PREFIX_QUESTION = 2; 
	public final static int AND_QUESTION = 3;
	public final static int NOT_QUESTION = 4;
	public final static int IN_SET_QUESTION = 5;

	public final static String XML_ELEMENT_NAME = "question";
	public final static String XML_ATTR_TYPE_NAME = "type";
	public final static String XML_ATTR_TIME_NAME = "time";
	public final static String XML_ATTR_ABOUT_HIDDEN_NAME = "is-about-hidden";
	public static final String XML_ATTR_IDX_NAME = "idx";

	private static Vector<Class<? extends Question>> questionTypes;
	//private LanguageModel LM = null;
	
	protected byte timeOffset = 0;
	protected final boolean aboutHidden;
	protected byte index = -1;
	
	public abstract boolean test(Context ctx);
	
	public static class TestResult {
		private boolean isSplit;
		private boolean result;        // only meaningful when isSplit = false
		private AbstractProbTree item; // only meaningful when isSplit = true
		private BinaryPrefix prefix;   // only meaningful when isSplit = true
		
		public TestResult(boolean result) {
			isSplit = false;
			this.result = result; 
		}
		
		public TestResult(AbstractProbTree item, BinaryPrefix prefix) {
			this.item = item;
			this.prefix = prefix;
			this.isSplit = true;
		}

		/**
		 * @return the isSplit
		 */
		public boolean isSplit() {
			return isSplit;
		}

		/**
		 * @return the result
		 */
		public boolean getResult() {
			return result;
		}

		protected void setResult(boolean res) {
			result = res;
		}
		/**
		 * @return the item
		 */
		public AbstractProbTree getItem() {
			return item;
		}

		/**
		 * @return the prefix
		 */
		public BinaryPrefix getPrefix() {
			return prefix;
		}

		@Override
		public String toString() {
			if (isSplit) {
				return "Split: prefix=" + prefix.toString();
			}
			return Boolean.toString(result);
		}
		
	}
	
	private static final AbstractProbTree[] dummyContext = new AbstractProbTree[0];
	
	public TestResult test(AbstractList<? extends AbstractProbTree> context) {
		return test(context.toArray(dummyContext));
	}
	
	public abstract TestResult test(AbstractProbTree[] context);
	
	public abstract TestResult test(AbstractProbTree item);
	
	static {
		Question.addQuestionType(IDENTITY_QUESTION, IdentityQuestion.class);
		Question.addQuestionType(BINARY_PREFIX_QUESTION, BinaryPrefixQuestion.class);
		Question.addQuestionType(AND_QUESTION, AndQuestion.class);
		Question.addQuestionType(NOT_QUESTION, NotQuestion.class);
		Question.addQuestionType(IN_SET_QUESTION, InSetQuestion.class);
	}
	/**
	 * @param timeOffset
	 * @param aboutHidden
	 * @return Boolean.TRUE if question @param q.test(tuple) => this.test(tuple),
	 *   Boolean.FALSE if q.test(tuple) => !this.test(tuple),
	 *   null otherwise
	 */
	public abstract Boolean implication(Question q, boolean value);
	
	public Question(byte timeOffset, boolean aboutHidden) {
		this.timeOffset = timeOffset;
		this.aboutHidden = aboutHidden;
	}
	
	public int getQuestionType() {return -1;}

	public Element createXML(Document doc) {
		Element element = doc.createElement(XML_ELEMENT_NAME);
		element.setAttribute(XML_ATTR_TYPE_NAME, Integer.toString(getQuestionType()));
		element.setAttribute(XML_ATTR_TIME_NAME, Integer.toString(timeOffset));
		element.setAttribute(XML_ATTR_ABOUT_HIDDEN_NAME, Boolean.toString(aboutHidden));
		element.setAttribute(XML_ATTR_IDX_NAME, Byte.toString(index));
		return element;
	}
	
	public Question(Element xmlnode) {
		assert(xmlnode.getTagName().equals(XML_ELEMENT_NAME));
		timeOffset = getTimeOffset(xmlnode);
		aboutHidden = getAboutHidden(xmlnode);
		index = getIndex(xmlnode);
	}
	
	public static Question fromXML(Element xmlnode) {
		assert(xmlnode.getTagName().equals(XML_ELEMENT_NAME));

		int type = Integer.parseInt(xmlnode.getAttributeNode(XML_ATTR_TYPE_NAME).getNodeValue());
		
		Class<? extends Question> _class = getQuestionType(type);
		try{
			Question q = (Question)_class.getDeclaredMethod("fromXML", Element.class).invoke(null, xmlnode);
			return q;
		} catch(Exception e) {
			e.printStackTrace();
			return null;
		}
	}
	
	protected static byte getTimeOffset(Element xmlnode) {
		return Byte.parseByte(xmlnode.getAttributeNode(XML_ATTR_TIME_NAME).getNodeValue());
	}
	
	protected static boolean getAboutHidden(Element xmlnode) {
		String s = xmlnode.getAttribute(XML_ATTR_ABOUT_HIDDEN_NAME);
		return s.equals("") ? false : Boolean.parseBoolean(s);
	}
	
	protected static byte getIndex(Element xmlnode) {
		String attr = xmlnode.getAttribute(XML_ATTR_IDX_NAME);
		return attr.isEmpty() ? -1 : Byte.parseByte(attr);
	}
	
	public static Class<? extends Question> getQuestionType(int type) {
		return questionTypes.get(type);
	}
	
	public static Enumeration<Class<? extends Question>> getQuestionTypes() {
		return questionTypes.elements();
	}
	
	protected static void addQuestionType(int type, Class<? extends Question> _class) {
		// I'm curious whether static initializers are guaranteed to be executed sequentially
		if (questionTypes == null) {
			questionTypes = new Vector<Class <? extends Question>>(Math.max(10, type+1));
		}
		if (type < questionTypes.size() && questionTypes.get(type) != null) {
			throw new Error("Duplicated question type: " + Integer.toString(type));
		}
		if (type >= questionTypes.size()) {
			questionTypes.setSize(type+1);
		}
		questionTypes.set(type, _class);
	}

	/**
	 * @return the timeOffset
	 */
	public byte getTimeOffset() {
		return timeOffset;
	}

	/* (non-Javadoc)
	 * @see edu.umd.clip.lm.util.XMLizable#getElementName()
	 */
	@Override
	public String getElementName() {
		return XML_ELEMENT_NAME;
	}
	
	public String toShortString() {
		return '[' + Integer.toString(timeOffset) + (aboutHidden ? ", hidden" : " word") + ']';  
	}

	/**
	 * @return the aboutHidden
	 */
	public boolean isAboutHidden() {
		return aboutHidden;
	}

	public byte getIndex() {
		return index;
	}
	
}
